namespace DataTanker.AccessMethods.RadixTree.Storage
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Diagnostics;

    using BinaryFormat.Page;
    using PageManagement;
    using MemoryManagement;

    /// <summary>
    /// Implementation of INodeStorage. 
    /// Instances of this class store nodes using specified object that implements IPageManager.
    /// </summary>
    class RadixTreeNodeStorage : INodeStorage
    {
        private readonly IPageManager _pageManager;
        private DbItemReference _rootNodeReference;

        private readonly LinkedList<long> _recentPages = new LinkedList<long>();
        private const int _maxRecentPages = 6;

        // this is used to reduce node reallocations
        private static readonly short _gapSize = (short) ((DbItemReference.BytesLength + 1) * 4);

        private short GetNodeSize(short prefixLength, byte entryCount)
        {
            var size =
             (short)(PageFormatter.OnPagePointerSize + // overall size
                     PageFormatter.OnPagePointerSize + // prefix size
                     prefixLength +                    // prefix itself   
                     DbItemReference.BytesLength +     // value reference
                     DbItemReference.BytesLength +     // parent node reference
                     1 +                               // entryCount
                     entryCount * (DbItemReference.BytesLength + 1)); // child node reference + symbol

            size += (short)(size % _gapSize);
            return size;
        }

        private byte[] GetNodeBytes(IRadixTreeNode node, short minimalSize)
        {
            var prefix = node.Prefix ?? new byte[0];
            var prefixLength = (short) prefix.Length;
            var valueReference = node.ValueReference ?? DbItemReference.Null;
            var parentReference = node.ParentNodeReference ?? DbItemReference.Null;

            var size = GetNodeSize(prefixLength, (byte) node.Entries.Count);

            var result = new byte[Math.Max(size, minimalSize)];

            using (var ms = new MemoryStream(result))
            {
                ms.Write(BitConverter.GetBytes(size), 0, PageFormatter.OnPagePointerSize);
                ms.Write(BitConverter.GetBytes(prefixLength), 0, PageFormatter.OnPagePointerSize);
                ms.Write(prefix, 0, prefixLength);
                valueReference.Write(ms);
                parentReference.Write(ms);
                ms.WriteByte((byte) node.Entries.Count);

                foreach (var entry in node.Entries)
                {
                    ms.WriteByte(entry.Key);
                    ms.Write(entry.Value.GetBytes(), 0, DbItemReference.BytesLength);
                }

                return ms.ToArray();
            }
        }

        private IRadixTreeNode NodeFromBytes(byte[] nodeBytes)
        {
            var result = new RadixTreeNode(0);
            using (var ms = new MemoryStream(nodeBytes, false))
            {
                var buffer = new byte[PageFormatter.OnPagePointerSize];

                ms.Read(buffer, 0, PageFormatter.OnPagePointerSize); // overall size

                ms.Read(buffer, 0, PageFormatter.OnPagePointerSize); // prefix length
                short prefixLength = BitConverter.ToInt16(buffer, 0);
                var prefix = new byte[prefixLength];
                ms.Read(prefix, 0, prefixLength); // prefix itself
                result.Prefix = prefix;
                result.Reference = DbItemReference.Read(ms);
                result.ParentNodeReference = DbItemReference.Read(ms);

                var nodeCount = ms.ReadByte();
                for (int i = 0; i < nodeCount; i++)
                {
                    var key = (byte) ms.ReadByte();
                    var value = DbItemReference.Read(ms);
                    result.Entries.Add(new KeyValuePair<byte, DbItemReference>(key, value));
                }

                return result;
            }
        }

        public IRadixTreeNode FetchRoot()
        {
            var headerPage = _pageManager.FetchPage(0);

            if(_rootNodeReference == null)
                _rootNodeReference = new DbItemReference(((HeadingPageHeader)PageFormatter.GetPageHeader(headerPage)).AccessMethodPageIndex, 0);

            return Fetch(_rootNodeReference);
        }

        public bool Update(IRadixTreeNode node)
        {
            byte[] nodeBytes = GetNodeBytes(node, 0);

            var page = _pageManager.FetchPage(node.Reference.PageIndex);
            if (PageFormatter.ReplaceVariableSizeItemIfPossible(page, node.Reference.ItemIndex, nodeBytes))
            {
                _pageManager.UpdatePage(page);
                return true;
            }

            return false;
        }

        private void UpdateRecentPage(long index)
        {
            if (_recentPages.Contains(index))
            {
                _recentPages.Remove(index);
            }
            else
            {
                if (_recentPages.Count == _maxRecentPages)
                    _recentPages.RemoveLast();
            }

            _recentPages.AddFirst(index);
        }

        public IRadixTreeNode Fetch(DbItemReference reference)
        {
            if (reference == null || reference.IsNull)
                return null;

            var page = _pageManager.FetchPage(reference.PageIndex);
            var nodeBytes = PageFormatter.ReadVariableSizeItem(page, reference.ItemIndex);

            UpdateRecentPage(reference.PageIndex);

            return NodeFromBytes(nodeBytes);
        }

        public IRadixTreeNode Create(int prefixSize, byte childCapacity)
        {
            IPage page;
            short itemIndex;
            var node = NewNode(DbItemReference.Null);
            var nodeSize = GetNodeSize((short) prefixSize, childCapacity);
            foreach (var pageIndex in _recentPages)
            {
                page = _pageManager.FetchPage(pageIndex);
                itemIndex = PageFormatter.AddVariableSizeItem(page, GetNodeBytes(node, nodeSize));
                if (itemIndex != -1)
                {
                    UpdateRecentPage(page.Index);
                    node.Reference = new DbItemReference(page.Index, itemIndex);
                    return node;
                }
            }

            page = _pageManager.CreatePage();

            itemIndex = PageFormatter.AddVariableSizeItem(page, GetNodeBytes(node, nodeSize));
            if (itemIndex != -1)
            {
                UpdateRecentPage(page.Index);
                node.Reference = new DbItemReference(page.Index, itemIndex);
                return node;
            }

            Debug.Fail("Should never get here");
            return null;
        }

        public void Remove(DbItemReference reference)
        {
            if (_rootNodeReference.Equals(reference))
                throw new ArgumentOutOfRangeException("reference", "Unable to delete the root node");
        }

        private IRadixTreeNode NewNode(DbItemReference parentReference)
        {
            return new RadixTreeNode(0)
            {
                ParentNodeReference = parentReference,
            };
        }

        public RadixTreeNodeStorage(IPageManager pageManager)
        {
            _pageManager = pageManager;

            // initialize and add root node
            var root = NewNode(DbItemReference.Null);
            _rootNodeReference = root.Reference;
        }
    }
}